﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using OSGeo.MapGuide;
using System.Drawing;
using System.ComponentModel;
using System.IO;
using System.Drawing.Drawing2D;

namespace MapViewer
{
    public class MgMapViewer : Control, IMapViewer
    {
        private BackgroundWorker renderWorker;

        private MgRenderingService _renderSvc;
        private MgdMap _map;
        private MgdSelection _selection;
        private MgRenderingOptions _layerRenderOpts;
        private Color _mapBgColor;

        private bool firstRun = true;

        private double _orgX1;
        private double _orgX2;
        private double _orgY1;
        private double _orgY2;

        private Image _image;

        internal Image Image
        {
            get { return _image; }
            set
            {
                _image = value;
                Invalidate();
            }
        }

        public MgMapViewer()
        {
            this.ActiveTool = MapActiveTool.None;
            this.DoubleBuffered = true;

            _mapBgColor = Color.Transparent;

            renderWorker = new BackgroundWorker();

            renderWorker.DoWork += renderWorker_DoWork;
            renderWorker.RunWorkerCompleted += renderWorker_RunWorkerCompleted;

            base.MouseUp += mapImg_MouseUp;
            base.MouseMove += mapImg_MouseMove;
            base.MouseDown += mapImg_MouseDown;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                base.MouseUp -= mapImg_MouseUp;
                base.MouseMove -= mapImg_MouseMove;
                base.MouseDown -= mapImg_MouseDown;

                renderWorker.DoWork -= renderWorker_DoWork;
                renderWorker.RunWorkerCompleted -= renderWorker_RunWorkerCompleted;

                _renderSvc.Dispose();
                _renderSvc = null;

                _selection.Dispose();
                _selection = null;
            }
            base.Dispose(disposing);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            if (_image == null)
            {
                base.OnPaintBackground(e);
                return;
            }

            if (!translate.IsEmpty)
            {
                e.Graphics.TranslateTransform(translate.X, translate.Y);
            }

            e.Graphics.DrawImage(_image, new PointF(0, 0));
            
            base.OnPaint(e);
        }

        public void Init(MgdMap map)
        {
            _renderSvc = MgServiceFactory.CreateRenderingService();
            _layerRenderOpts = new MgRenderingOptions(MgImageFormats.Png, 7, new MgColor(0, 0, 255));

            _map = map;
            var bgColor = _map.GetBackgroundColor();
            if (bgColor.Length == 8 || bgColor.Length == 6)
            {
                _mapBgColor = ColorTranslator.FromHtml("#" + bgColor);
                this.BackColor = _mapBgColor;
            }
            _map.SetDisplaySize(this.Width, this.Height);
            _selection = new MgdSelection(_map);

            if (firstRun)
            {
                this.Resize += new EventHandler(OnControlResized);
                firstRun = false;
            }

            var env = _map.GetMapExtent();
            var ll = env.LowerLeftCoordinate;
            var ur = env.UpperRightCoordinate;

            _orgX1 = ll.X;
            _orgY2 = ll.Y;
            _orgX2 = ur.X;
            _orgY1 = ur.Y;

            if ((_orgX1 - _orgX2) == 0 || (_orgY1 - _orgY2) == 0)
            {
                _orgX1 = -.1;
                _orgX2 = .1;
                _orgY1 = -.1;
                _orgY2 = .1;
            }

            double scale = CalculateScale(Math.Abs(_orgX2 - _orgX1), Math.Abs(_orgY2 - _orgY1), this.Width, this.Height);
            _map.SetViewScale(scale);

            var handler = this.ViewerInitialized;
            if (handler != null)
                handler(this, EventArgs.Empty);
        }

        private double CalculateScale(double mcsW, double mcsH, int devW, int devH)
        {
            var mpu = _map.GetMetersPerUnit();
            var mpp = 0.0254 / _map.DisplayDpi;
            if (devH * mcsW > devW * mcsH)
                return mcsW * mpu / (devW * mpp); //width-limited
            else
                return mcsH * mpu / (devH * mpp); //height-limited
        }

        public event EventHandler ViewerInitialized;

        void OnControlResized(object sender, EventArgs e)
        {
            _map.SetDisplaySize(this.Width, this.Height);
            RefreshMap(false);
        }

        public void ClearSelection()
        {
            //TODO: Add a Clear() method to MgdSelection

            if (_selection != null)
                _selection.Dispose();

            _selection = new MgdSelection(_map);
        }

        public MgdMap GetMap()
        {
            return _map;
        }

        public MgdSelection GetSelection()
        {
            return _selection;
        }

        private bool _digitizing = false;

        public bool IsDigitizing
        {
            get { return _digitizing; }
            private set
            {
                if (_digitizing.Equals(value))
                    return;

                _digitizing = value;
                var handler = this.DigitizationStateChanged;
                if (handler != null)
                    handler(this, EventArgs.Empty);
            }
        }

        public event EventHandler DigitizationStateChanged;

        class RenderWorkArgs
        {
            public MgRenderingOptions RenderingOptions { get; set; }

            public bool RaiseEvents { get; set; }
        }

        class RenderResult
        {
            public Image Image { get; set; }

            public bool RaiseEvents { get; set; }
        }

        public void RefreshMap(bool raiseEvents)
        {
            //TODO: Queue the refresh map request
            if (this.IsBusy)
                return;

            if (this.Image != null)
            {
                this.Image.Dispose();
                this.Image = null;
            }

            this.IsBusy = true;
            renderWorker.RunWorkerAsync(new RenderWorkArgs() { RenderingOptions = _layerRenderOpts, RaiseEvents = raiseEvents });
        }

        public event EventHandler MapRefreshed;

        private bool _busy = false;

        public bool IsBusy
        {
            get { return _busy; }
            private set
            {
                if (_busy.Equals(value))
                    return;

                _busy = value;
                System.Diagnostics.Trace.TraceInformation("IsBusy = " + _busy);

                var handler = this.BusyStateChanged;
                if (handler != null)
                    handler(this, EventArgs.Empty);
            }
        }

        public event EventHandler BusyStateChanged;

        public void ZoomToView(double x, double y, double scale, bool refresh)
        {
            _map.SetViewCenterXY(x, y);
            _map.SetViewScale(scale);
            if (refresh)
                RefreshMap(true);
        }

        public event EventHandler MapScaleChanged;

        public event EventHandler SelectionChanged;

        private void renderWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            var args = (RenderWorkArgs)e.Argument;

            var br = _renderSvc.RenderDynamicOverlay(_map, _selection, args.RenderingOptions);
            byte[] b = new byte[br.GetLength()];
            br.Read(b, b.Length);
            using (var ms = new MemoryStream(b))
            {
                e.Result = new RenderResult() { Image = Image.FromStream(ms), RaiseEvents = args.RaiseEvents };
            }
        }

        private void renderWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            this.IsBusy = AreWorkersBusy();
            if (e.Error != null)
            {
                MessageBox.Show(e.Error.Message, "Error");
            }
            else
            {
                var res = (RenderResult)e.Result;
                //reset translation
                translate = new System.Drawing.Point();
                //set the image
                this.Image = res.Image;

                var center = _map.ViewCenter;
                var ext = _map.DataExtent;
                System.Diagnostics.Trace.TraceInformation(
                    "**POST-RENDER**{2}Map Center: {0}, {1}{2}Lower left: {3}, {4}{2}Upper Right: {5}, {6}",
                    center.Coordinate.X,
                    center.Coordinate.Y,
                    Environment.NewLine,
                    ext.LowerLeftCoordinate.X,
                    ext.LowerLeftCoordinate.Y,
                    ext.UpperRightCoordinate.X,
                    ext.UpperRightCoordinate.Y);

                if (res.RaiseEvents)
                {
                    var handler = this.MapRefreshed;
                    if (handler != null)
                        handler(this, EventArgs.Empty);
                }
            }
        }

        private bool AreWorkersBusy()
        {
            return renderWorker.IsBusy;
        }

        public void InitialMapView()
        {
            var scale = CalculateScale((_orgX2 - _orgX1), (_orgY1 - _orgY2), this.Width, this.Height);
            ZoomToView(_orgX1 + ((_orgX2 - _orgX1) / 2), _orgY2 + ((_orgY1 - _orgY2) / 2), scale, true);
        }

        private void mapImg_Click(object sender, EventArgs e)
        {
            HandleClick();
        }

        private void mapImgOverlay_Click(object sender, EventArgs e)
        {
            HandleClick();
        }

        private void HandleClick()
        {

        }

        private void mapImg_MouseDown(object sender, MouseEventArgs e)
        {
            HandleMouseDown(e);
        }

        private void mapImg_MouseMove(object sender, MouseEventArgs e)
        {
            HandleMouseMove(e);
        }
        
        private void mapImg_MouseUp(object sender, MouseEventArgs e)
        {
            HandleMouseUp(e);
        }

        private void HandleMouseDown(MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                dragStart = e.Location;
                System.Diagnostics.Trace.TraceInformation("Drag started at (" + dragStart.X + ", " + dragStart.Y + ")");
            }
        }

        private System.Drawing.Point translate;

        private System.Drawing.Point dragStart;
        bool isDragging = false;
        Image mouseDragImg;

        private void HandleMouseMove(MouseEventArgs e)
        {
            var mapPt = ScreenToMapUnits(e.X, e.Y);
            OnMouseMapPositionChanged(mapPt.X, mapPt.Y);

            if (this.ActiveTool == MapActiveTool.Pan)
            {
                if (e.Location != dragStart && !isDragging && e.Button == MouseButtons.Left)
                {
                    isDragging = true;
                }

                if (isDragging)
                {
                    translate = new System.Drawing.Point(e.X - dragStart.X, e.Y - dragStart.Y);
                    Invalidate();
                }
            }
        }

        private void HandleMouseUp(MouseEventArgs e)
        {
            if (isDragging)
            {
                isDragging = false;

                System.Diagnostics.Trace.TraceInformation("Dragged screen distance (" + translate.X + ", " + translate.Y + ")");

                int dx = e.X - dragStart.X;
                int dy = e.Y - dragStart.Y;

                //FIXME:
                //
                // One of these is FUBAR:
                // 1. The center screen coordinate
                // 2. The translation being applied to the centre screen coordinate
                // 3. The ScreenToMapUnits() function
                //
                //We are panning in the right direction, but the refreshed result does not
                //reflect where we dragged the image to.
                var centerScreen = new Point(this.Location.X + (this.Width / 2), this.Location.Y + (this.Height / 2));

                centerScreen.X -= translate.X;
                centerScreen.Y -= translate.Y;

                var pt = _map.ViewCenter.Coordinate;
                var coord = ScreenToMapUnits(centerScreen.X, centerScreen.Y);

                double mdx = coord.X - pt.X;
                double mdy = coord.Y - pt.Y;

                System.Diagnostics.Trace.TraceInformation("Shift map center by (" + mdx + ", " + mdy + ") (" + GetXDirection(pt.X, coord.X) + ", " + GetYDirection(pt.Y, coord.Y) + ")");
                //System.Diagnostics.Trace.TraceInformation("Set map center (" + pt.X + ", " + pt.Y + ") -> (" + coord.X + ", " + coord.Y + ") (" + GetXDirection(pt.X, coord.X) + ", " + GetYDirection(pt.Y, coord.Y) + ")");
                _map.SetViewCenterXY(coord.X, coord.Y);

                var center = _map.ViewCenter;
                var ext = _map.DataExtent;
                System.Diagnostics.Trace.TraceInformation(
                    "**PRE-RENDER**{2}Map Center: {0}, {1}{2}Lower left: {3}, {4}{2}Upper Right: {5}, {6}",
                    center.Coordinate.X,
                    center.Coordinate.Y,
                    Environment.NewLine,
                    ext.LowerLeftCoordinate.X,
                    ext.LowerLeftCoordinate.Y,
                    ext.UpperRightCoordinate.X,
                    ext.UpperRightCoordinate.Y);

                RefreshMap(false);
            }
        }

        private string GetYDirection(double oldY, float newY)
        {
            if (newY > oldY)
                return "UP";
            else
                return "DOWN";
        }

        private string GetXDirection(double oldX, float newX)
        {
            if (newX > oldX)
                return "RIGHT";
            else
                return "LEFT";
        }

        private MapActiveTool _tool;

        public MapActiveTool ActiveTool
        {
            get
            {
                return _tool;
            }
            set
            {
                if (_tool.Equals(value))
                    return;

                _tool = value;
                var handler = this.MapActiveToolChanged;
                if (handler != null)
                    handler(this, EventArgs.Empty);
            }
        }

        public event EventHandler MapActiveToolChanged;

        public PointF ScreenToMapUnits(double x, double y)
        {
            return ScreenToMapUnits(x, y, false);
        }

        private PointF ScreenToMapUnits(double x, double y, bool allowOutsideWindow)
        {
            if (!allowOutsideWindow)
            {
                if (x > this.Width - 1) x = this.Width - 1;
                else if (x < 0) x = 0;

                if (y > this.Height - 1) y = this.Height - 1;
                else if (y < 0) y = 0;
            }

            x = _orgX1 + (_orgX2 - _orgX1) * (x / this.Width);
            y = _orgY1 - (_orgY1 - _orgY2) * (y / this.Height);
            return new PointF((float)x, (float)y);
        }

        private void OnMouseMapPositionChanged(double x, double y)
        {
            var handler = this.MouseMapPositionChanged;
            if (handler != null)
                handler(this, new MapPointEventArgs(x, y));
        }

        public event EventHandler<MapPointEventArgs> MouseMapPositionChanged;
    }
}
